/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.videoeditor.service;

import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.media.videoeditor.MediaItem;
import android.media.videoeditor.MediaItem.GetThumbnailListCallback;
import android.media.videoeditor.MediaProperties;
import android.media.videoeditor.MediaVideoItem;
import android.media.videoeditor.VideoEditor;
import android.media.videoeditor.VideoEditor.ExportProgressListener;
import android.media.videoeditor.VideoEditorFactory;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.util.Log;

import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

/**
 * VideoEditor service API
 */
public class ApiService extends Service {
    // Logging
    private static final String TAG = "VEApiService";

    // Parameters
    private static final String PARAM_OP = "op";
    private static final String PARAM_REQUEST_ID = "rid";
    private static final String PARAM_PROJECT_PATH = "project";
    private static final String PARAM_FILENAME = "filename";
    private static final String PARAM_STORYBOARD_ITEM_ID = "item_id";
    private static final String PARAM_PROGRESS_VALUE = "prog_value";
    private static final String PARAM_EXCEPTION = "ex";
    private static final String PARAM_START_TIME = "s_time";
    private static final String PARAM_END_TIME = "e_time";
    private static final String PARAM_WIDTH = "width";
    private static final String PARAM_HEIGHT = "height";
    private static final String PARAM_BITRATE = "bitrate";
    private static final String PARAM_AUDIO_CODEC = "audio_codec";
    private static final String PARAM_VIDEO_CODEC = "video_codec";
    private static final String PARAM_INTENT = "req_intent";
    private static final String PARAM_MOVIE_FILENAME = "movie";
    private static final String PARAM_ASPECT_RATIO = "aspect_ratio";
    private static final String PARAM_COUNT = "count";
    private static final String PARAM_TOKEN = "token";
    private static final String PARAM_INDICES = "indices";
    private static final String PARAM_CANCELLED = "cancelled";

    // Operations
    private static final int OP_VIDEO_EDITOR_CREATE = 1;
    private static final int OP_VIDEO_EDITOR_EXPORT = 4;
    private static final int OP_VIDEO_EDITOR_CANCEL_EXPORT = 5;
    private static final int OP_VIDEO_EDITOR_EXPORT_STATUS = 6;
    private static final int OP_VIDEO_EDITOR_DELETE = 9;
    private static final int OP_VIDEO_EDITOR_SET_ASPECT_RATIO = 10;

    private static final int OP_MEDIA_ITEM_GET_THUMBNAILS = 112;

    // Static member variables
    private static final Map<String, Intent> mPendingIntents = new HashMap<String, Intent>();
    private static final List<ApiServiceListener> mListeners = new ArrayList<ApiServiceListener>();
    private static final IntentPool mIntentPool = new IntentPool(8);
    private static VideoEditor mVideoEditor;
    private static volatile boolean mExportCancelled;

    private IntentProcessor mVideoThread;
    private IntentProcessor mThumbnailThread;
    private Handler mHandler;

    private final Runnable mStopRunnable = new Runnable() {
        @Override
        public void run() {
            if (mPendingIntents.size() == 0) {
                Log.d(TAG, "Stop runnable: Stopping service");
                stopSelf();
            }
        }
    };

    /**
     * @return A unique id
     */
    public static String generateId() {
        return randomString(6);
    }

    /**
     * Register a listener
     *
     * @param listener The listener
     */
    public static void registerListener(ApiServiceListener listener) {
        mListeners.add(listener);
    }

    /**
     * Unregister a listener
     *
     * @param listener The listener
     */
    public static void unregisterListener(ApiServiceListener listener) {
        mListeners.remove(listener);
    }

    /**
     * Create a new VideoEditor project
     *
     * @param context The context
     * @param projectPath The project path
     * @param projectName The project name
     * @param movies The movie file name to add to the newly created project
     */
    public static void createVideoEditor(Context context, String projectPath, String projectName,
                String movieName) {
        final Intent intent = mIntentPool.get(context, ApiService.class);
        intent.putExtra(PARAM_OP, OP_VIDEO_EDITOR_CREATE);
        intent.putExtra(PARAM_PROJECT_PATH, projectPath);
        intent.putExtra(PARAM_MOVIE_FILENAME, movieName);

        startCommand(context, intent);
    }

    /**
     * Export the VideoEditor movie
     *
     * @param context The context
     * @param projectPath The project path
     * @param filename The export filename
     * @param height The output movie height
     * @param bitrate The output movie bitrate
     */
    public static void exportVideoEditor(Context context, String projectPath, String filename,
            int height, int bitrate, int audioCodec, int videoCodec) {
        final Intent intent = mIntentPool.get(context, ApiService.class);
        intent.putExtra(PARAM_OP, OP_VIDEO_EDITOR_EXPORT);
        intent.putExtra(PARAM_PROJECT_PATH, projectPath);
        intent.putExtra(PARAM_FILENAME, filename);
        intent.putExtra(PARAM_HEIGHT, height);
        intent.putExtra(PARAM_BITRATE, bitrate);
        intent.putExtra(PARAM_AUDIO_CODEC, audioCodec);
        intent.putExtra(PARAM_VIDEO_CODEC, videoCodec);

        startCommand(context, intent);
    }

    /**
     * Check if export is pending
     *
     * @param projectPath The project path
     * @param filename The export filename
     *
     * @return true if the export is pending
     */
    public static boolean isVideoEditorExportPending(String projectPath, String filename) {
        for (Intent intent : mPendingIntents.values()) {
            final int op = intent.getIntExtra(PARAM_OP, -1);
            if (op == OP_VIDEO_EDITOR_EXPORT) {
                String pp = intent.getStringExtra(PARAM_PROJECT_PATH);
                if (pp.equals(projectPath)) {
                    String fn = intent.getStringExtra(PARAM_FILENAME);
                    if (fn.equals(filename)) {
                        return true;
                    }
                }
            }
        }

        return false;
    }

    /**
     * Cancel the export of the specified VideoEditor movie
     *
     * @param context The context
     * @param projectPath The project path
     * @param filename The export filename
     */
    public static void cancelExportVideoEditor(Context context, String projectPath,
            String filename) {
        mExportCancelled = true;
        final Intent intent = mIntentPool.get(context, ApiService.class);
        intent.putExtra(PARAM_OP, OP_VIDEO_EDITOR_CANCEL_EXPORT);
        intent.putExtra(PARAM_PROJECT_PATH, projectPath);
        intent.putExtra(PARAM_FILENAME, filename);

        startCommand(context, intent);
    }

    /**
     * Change the aspect ratio
     *
     * @param context The context
     * @param projectPath The project path
     * @param aspectRatio The aspect ratio
     */
    public static void setAspectRatio(Context context, String projectPath, int aspectRatio) {
        final Intent intent = mIntentPool.get(context, ApiService.class);
        intent.putExtra(PARAM_OP, OP_VIDEO_EDITOR_SET_ASPECT_RATIO);
        intent.putExtra(PARAM_PROJECT_PATH, projectPath);
        intent.putExtra(PARAM_ASPECT_RATIO, aspectRatio);

        startCommand(context, intent);
    }

    /**
     * Delete the project specified by the project path
     *
     * @param context The context
     * @param projectPath The project path
     */
    public static void deleteProject(Context context, String projectPath) {
        final Intent intent = mIntentPool.get(context, ApiService.class);
        intent.putExtra(PARAM_OP, OP_VIDEO_EDITOR_DELETE);
        intent.putExtra(PARAM_PROJECT_PATH, projectPath);

        startCommand(context, intent);
    }

    /**
     * Get the thumbnails of the specified size
     *
     * @param context The context
     * @param projectPath The project path
     * @param mediaItemId The id of the media item
     * @param width The width
     * @param height The height
     * @param startMs The start time in milliseconds
     * @param endMs The end time in milliseconds
     * @param count The number of thumbnails
     */
    public static void getMediaItemThumbnails(Context context,
            String projectPath, String mediaItemId, int width, int height,
            long startMs, long endMs, int count, int token, int[] indices) {
        final Intent intent = mIntentPool.get(context, ApiService.class);
        intent.putExtra(PARAM_OP, OP_MEDIA_ITEM_GET_THUMBNAILS);
        intent.putExtra(PARAM_PROJECT_PATH, projectPath);
        intent.putExtra(PARAM_STORYBOARD_ITEM_ID, mediaItemId);
        intent.putExtra(PARAM_WIDTH, width);
        intent.putExtra(PARAM_HEIGHT, height);
        intent.putExtra(PARAM_START_TIME, startMs);
        intent.putExtra(PARAM_END_TIME, endMs);
        intent.putExtra(PARAM_COUNT, count);
        intent.putExtra(PARAM_TOKEN, token);
        intent.putExtra(PARAM_INDICES, indices);

        startCommand(context, intent);
    }

    /**
     * Start the service (if it is not running) with the specified Intent
     *
     * @param context The context
     * @param intent The intent
     *
     * @return The request id of the pending request
     */
    private static String startCommand(Context context, Intent intent) {
        final String requestId = randomString(8);
        intent.putExtra(PARAM_REQUEST_ID, requestId);
        mPendingIntents.put(requestId, intent);

        context.startService(intent);

        return requestId;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        mHandler = new Handler(Looper.getMainLooper());

        mVideoThread = new IntentProcessor("VideoServiceThread");
        mVideoThread.start();

        mThumbnailThread = new IntentProcessor("ThumbnailServiceThread");
        mThumbnailThread.start();
    }

    @Override
    public int onStartCommand(final Intent intent, int flags, int startId) {
        final int op = intent.getIntExtra(PARAM_OP, -1);
        switch(op) {
            case OP_VIDEO_EDITOR_CREATE:
            case OP_VIDEO_EDITOR_DELETE:
            case OP_VIDEO_EDITOR_SET_ASPECT_RATIO:
            case OP_VIDEO_EDITOR_EXPORT:
            case OP_VIDEO_EDITOR_CANCEL_EXPORT:
            case OP_VIDEO_EDITOR_EXPORT_STATUS: {
                mVideoThread.submit(intent);
                break;
            }

            case OP_MEDIA_ITEM_GET_THUMBNAILS: {
                final String projectPath = intent.getStringExtra(PARAM_PROJECT_PATH);
                final String mediaItemId = intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID);
                final int token = intent.getIntExtra(PARAM_TOKEN, 0);
                // Cancel any pending thumbnail request for the same media item
                // but with a different token
                Iterator<Intent> intentQueueIterator = mThumbnailThread.getIntentQueueIterator();
                while (intentQueueIterator.hasNext()) {
                    Intent qIntent = intentQueueIterator.next();
                    int opi = qIntent.getIntExtra(PARAM_OP, -1);
                    String pp = qIntent.getStringExtra(PARAM_PROJECT_PATH);
                    String mid = qIntent.getStringExtra(PARAM_STORYBOARD_ITEM_ID);
                    int tk = qIntent.getIntExtra(PARAM_TOKEN, 0);
                    if (opi == op && pp.equals(projectPath) && mid.equals(mediaItemId)
                            && tk != token) {
                        boolean canceled = mThumbnailThread.cancel(qIntent);
                        if (canceled) {
                            Log.d(TAG, "Canceled operation: " + op + " for media item " + mediaItemId);
                            mPendingIntents.remove(qIntent.getStringExtra(PARAM_REQUEST_ID));
                            mIntentPool.put(qIntent);
                        }
                        break;
                    }
                }
                mThumbnailThread.submit(intent);
                break;
            }

            default: {
                Log.e(TAG, "No thread assigned: " + op);
                break;
            }
        }

        return START_NOT_STICKY;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();

        if (mThumbnailThread != null) {
            mThumbnailThread.quit();
            mThumbnailThread = null;
        }

        if (mVideoThread != null) {
            mVideoThread.quit();
            mVideoThread = null;
        }
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    /**
     * Process the intent
     *
     * @param intent The intent
     */
    public void processIntent(final Intent intent) {
        final int op = intent.getIntExtra(PARAM_OP, -1);
        VideoEditor videoEditor = null;
        try {
            final String projectPath = intent.getStringExtra(PARAM_PROJECT_PATH);
            // Check if the project path matches the current VideoEditor project
            switch (op) {
                case OP_VIDEO_EDITOR_CREATE:
                case OP_VIDEO_EDITOR_DELETE: {
                    break;
                }

                default: {
                    videoEditor = getVideoEditor(projectPath);
                    if (videoEditor == null) {
                        throw new IllegalArgumentException("Invalid project path: "
                                + projectPath + " for operation: " + op);
                    }
                    break;
                }
            }

            switch (op) {
                case OP_VIDEO_EDITOR_CREATE: {
                    Log.d(TAG, "OP_VIDEO_EDITOR_CREATE: " + projectPath);

                    try {
                        // Release the current video editor if any
                        releaseEditor();

                        videoEditor = VideoEditorFactory.create(projectPath);

                        // Add the movie to the timeline
                        final String movieName = intent.getStringExtra(PARAM_MOVIE_FILENAME);
                        final MediaItem mediaItem = new MediaVideoItem(videoEditor,
                                generateId(), movieName, MediaItem.RENDERING_MODE_BLACK_BORDER);
                        videoEditor.addMediaItem(mediaItem);

                        // Set the aspect ratio to the aspect ratio of the first item
                        final List<MediaItem> mediaItems = videoEditor.getAllMediaItems();
                        if (mediaItems.size() > 0) {
                            videoEditor.setAspectRatio(mediaItems.get(0).getAspectRatio());
                        }

                        // Make this project the current project
                        mVideoEditor = videoEditor;

                        completeRequest(intent, videoEditor, null, null, null, false);
                        completeRequest(intent);
                    } catch (Exception ex) {
                        if (videoEditor != null) {
                            videoEditor.release();
                            videoEditor = null;
                        }
                        throw ex;
                    }

                    break;
                }

                case OP_VIDEO_EDITOR_SET_ASPECT_RATIO: {
                    Log.d(TAG, "OP_VIDEO_EDITOR_SET_ASPECT_RATIO");

                    videoEditor.setAspectRatio(intent.getIntExtra(PARAM_ASPECT_RATIO,
                            MediaProperties.ASPECT_RATIO_UNDEFINED));

                    completeRequest(intent, videoEditor, null, null, null, false);
                    completeRequest(intent);
                    break;
                }

                case OP_VIDEO_EDITOR_EXPORT: {
                    Log.d(TAG, "OP_VIDEO_EDITOR_EXPORT");
                    exportMovie(videoEditor, intent);
                    break;
                }

                case OP_VIDEO_EDITOR_CANCEL_EXPORT: {
                    Log.d(TAG, "OP_VIDEO_EDITOR_CANCEL_EXPORT");
                    videoEditor.cancelExport(intent.getStringExtra(PARAM_FILENAME));
                    completeRequest(intent, videoEditor, null, null, null, true);
                    break;
                }

                case OP_VIDEO_EDITOR_EXPORT_STATUS: {
                    Log.d(TAG, "OP_VIDEO_EDITOR_EXPORT_STATUS");
                    completeRequest(intent, videoEditor, null, null, null, true);
                    break;
                }

                case OP_VIDEO_EDITOR_DELETE: {
                    Log.d(TAG, "OP_VIDEO_EDITOR_DELETE: " + projectPath);
                    releaseEditor(projectPath);
                    // Delete all the files and the project folder.
                    deleteDir(new File(projectPath));
                    completeRequest(intent, videoEditor, null, null, null, true);
                    break;
                }

                case OP_MEDIA_ITEM_GET_THUMBNAILS: {
                    // Note that this command is executed in the thumbnail thread
                    final String mediaItemId = intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID);
                    Log.d(TAG, "OP_MEDIA_ITEM_GET_THUMBNAILS: " + mediaItemId);

                    final MediaItem mediaItem = videoEditor.getMediaItem(mediaItemId);
                    if (mediaItem == null) {
                        throw new IllegalArgumentException("MediaItem not found: " + mediaItemId);
                    }

                    final VideoEditor ve = videoEditor; // Just to make it "final"
                    mediaItem.getThumbnailList(
                            intent.getIntExtra(PARAM_WIDTH, 0),
                            intent.getIntExtra(PARAM_HEIGHT, 0),
                            intent.getLongExtra(PARAM_START_TIME, 0),
                            intent.getLongExtra(PARAM_END_TIME, 0),
                            intent.getIntExtra(PARAM_COUNT, 0),
                            intent.getIntArrayExtra(PARAM_INDICES),
                            new GetThumbnailListCallback() {
                                public void onThumbnail(Bitmap bitmap, int index) {
                                    completeRequest(
                                            intent, ve, null, bitmap,
                                            Integer.valueOf(index), false);
                                }
                            }
                            );

                    completeRequest(intent, videoEditor, null, null, null, true);
                    break;
                }

                default: {
                    throw new IllegalArgumentException("Unhandled operation: " + op);
                }
            }
        } catch (Exception ex) {
            ex.printStackTrace();
            completeRequest(intent, videoEditor, ex, null, null, true);
        }
    }

    /**
     * Complete the request
     *
     * @param intent The intent
     * @param videoEditor The video editor
     * @param exception The exception
     * @param result The result object
     * @param extraResult The extra result object
     * @param finalize true if the request should be finalized
     */
    private void completeRequest(final Intent intent, final VideoEditor videoEditor,
            final Exception exception, final Object result, final Object extraResult,
            final boolean finalize) {
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                onIntentProcessed(intent, videoEditor, result, extraResult, exception, finalize);
            }
        });
    }

    /**
     * Complete the request
     *
     * @param intent The intent
     */
    private void completeRequest(final Intent intent) {
        mHandler.post (new Runnable() {
            @Override
            public void run() {
                finalizeRequest(intent);
                mIntentPool.put(intent);
            }
        });
    }

    /**
     * Callback called after the specified intent is processed.
     *
     * @param intent The intent
     * @param videoEditor The VideoEditor on which the operation was performed
     * @param result The result object
     * @param extraResult The extra result object
     * @param ex The exception
     * @param finalize true if the request should be finalized
     */
    public void onIntentProcessed(final Intent intent, VideoEditor videoEditor,
            Object result, Object extraResult, Exception ex, boolean finalize) {

        final String projectPath = intent.getStringExtra(PARAM_PROJECT_PATH);
        final int op = intent.getIntExtra(PARAM_OP, -1);
        switch (op) {
            case OP_VIDEO_EDITOR_CREATE: {
                if (finalize) {
                    finalizeRequest(intent);
                }

                if (ex != null) {
                    deleteDir(new File(projectPath));
                }

                for (ApiServiceListener listener : mListeners) {
                    listener.onVideoEditorCreated(projectPath,
                            videoEditor != null ? videoEditor.getAllMediaItems() : null, ex);
                }

                break;
            }

            case OP_VIDEO_EDITOR_SET_ASPECT_RATIO: {
                if (finalize) {
                    finalizeRequest(intent);
                }

                final int aspectRatio = intent.getIntExtra(PARAM_ASPECT_RATIO,
                        MediaProperties.ASPECT_RATIO_UNDEFINED);
                for (ApiServiceListener listener : mListeners) {
                    listener.onVideoEditorAspectRatioSet(projectPath, aspectRatio, ex);
                }

                break;
            }

            case OP_VIDEO_EDITOR_EXPORT: {
                // The finalizeRequest() call and listener callbacks are done in
                // OP_VIDEO_EDITOR_EXPORT_STATUS intent handling (where we are
                // called originalIntent).
                break;
            }

            case OP_VIDEO_EDITOR_CANCEL_EXPORT: {
                if (finalize) {
                    finalizeRequest(intent);
                }

                for (ApiServiceListener listener : mListeners) {
                    listener.onVideoEditorExportCanceled(projectPath,
                            intent.getStringExtra(PARAM_FILENAME));
                }
                break;
            }

            case OP_VIDEO_EDITOR_EXPORT_STATUS: {
                // This operation is for the service internal use only
                if (finalize) {
                    finalizeRequest(intent);
                }

                final String filename = intent.getStringExtra(PARAM_FILENAME);
                if (intent.hasExtra(PARAM_EXCEPTION)) { // Complete
                    final Intent originalIntent = (Intent)intent.getParcelableExtra(PARAM_INTENT);
                    finalizeRequest(originalIntent);
                    mIntentPool.put(originalIntent);

                    final Exception exception =
                        (Exception)intent.getSerializableExtra(PARAM_EXCEPTION);
                    final boolean cancelled = intent.getBooleanExtra(PARAM_CANCELLED, false);

                    for (ApiServiceListener listener : mListeners) {
                        listener.onVideoEditorExportComplete(
                                projectPath, filename, exception, cancelled);
                    }
                } else { // Progress
                    for (ApiServiceListener listener : mListeners) {
                        listener.onVideoEditorExportProgress(projectPath, filename,
                                intent.getIntExtra(PARAM_PROGRESS_VALUE, -1));
                    }

                    // The original request is still pending
                }
                break;
            }

            case OP_VIDEO_EDITOR_DELETE: {
                if (finalize) {
                    finalizeRequest(intent);
                }

                for (ApiServiceListener listener : mListeners) {
                    listener.onVideoEditorDeleted(projectPath, ex);
                }

                break;
            }

            case OP_MEDIA_ITEM_GET_THUMBNAILS: {
                if (finalize) {
                    finalizeRequest(intent);
                    break;
                }

                final Bitmap bitmap = (Bitmap)result;
                final int index = (Integer)extraResult;
                boolean used = false;
                for (ApiServiceListener listener : mListeners) {
                    used |= listener.onMediaItemThumbnail(projectPath,
                            intent.getStringExtra(PARAM_STORYBOARD_ITEM_ID),
                            bitmap, index, intent.getIntExtra(PARAM_TOKEN, 0),
                            ex);
                }

                if (used == false) {
                    if (bitmap != null) {
                        bitmap.recycle();
                    }
                }

                break;
            }

            default: {
                if (finalize) {
                    finalizeRequest(intent);
                }
                break;
            }
        }

        if (finalize) {
            mIntentPool.put(intent);
        }
    }

    /**
     * Finalizes a request. Calls the listeners that are interested in project status
     * change and stops this service if there are no more pending intents.
     *
     * @param intent The intent that just completed
     */
    private void finalizeRequest(Intent intent) {
        mPendingIntents.remove(intent.getStringExtra(PARAM_REQUEST_ID));

        if (mPendingIntents.size() == 0) {
            // Cancel the current timer if any. Extend the timeout by 5000 ms.
            mHandler.removeCallbacks(mStopRunnable);

            // Start a timer which will stop the service if the queue of
            // pending intent will be empty at that time.
            // This prevents the service from starting & stopping too often.
            mHandler.postDelayed(mStopRunnable, 5000);
            Log.d(TAG, "completeRequest: Stopping service in 5000 ms");
        }
    }

    /**
     * Check if the current editor is the project specified by the specified path
     *
     * @param projectPath The project path
     *
     * @return The video editor
     */
    private synchronized VideoEditor getVideoEditor(String projectPath) {
        if (mVideoEditor != null) {
            if (mVideoEditor.getPath().equals(projectPath)) {
                return mVideoEditor;
            }
        }

        return null;
    }

    /**
     * Release the editor
     */
    private synchronized void releaseEditor() {
        if (mVideoEditor != null) {
            Log.d(TAG, "releaseEditor (current): " + mVideoEditor.getPath());
            mVideoEditor.release();
            mVideoEditor = null;

            System.gc();
        }
    }

    /**
     * Release the specified editor
     *
     * @param projectPath The project path
     */
    private synchronized void releaseEditor(String projectPath) {
        if (mVideoEditor != null) {
            if (mVideoEditor.getPath().equals(projectPath)) {
                Log.d(TAG, "releaseEditor: " + projectPath);
                mVideoEditor.release();
                mVideoEditor = null;

                System.gc();
            }
        }
    }

    /**
     * Exports a movie in a distinct worker thread.
     *
     * @param videoEditor The video editor
     * @param intent The intent
     */
    private void exportMovie(final VideoEditor videoEditor, final Intent intent) {
        mExportCancelled = false;
        new Thread() {
            @Override
            public void run() {
                final String filename = intent.getStringExtra(PARAM_FILENAME);
                final int height = intent.getIntExtra(PARAM_HEIGHT, -1);
                final int bitrate = intent.getIntExtra(PARAM_BITRATE, -1);
                final int audioCodec = intent.getIntExtra(PARAM_AUDIO_CODEC, -1);
                final int videoCodec = intent.getIntExtra(PARAM_VIDEO_CODEC, -1);

                // Create the export status Intent
                final Intent statusIntent = mIntentPool.get();
                statusIntent.putExtra(PARAM_OP, OP_VIDEO_EDITOR_EXPORT_STATUS);
                statusIntent.putExtra(PARAM_PROJECT_PATH, intent.getStringExtra(
                        PARAM_PROJECT_PATH));
                statusIntent.putExtra(PARAM_FILENAME, filename);
                statusIntent.putExtra(PARAM_INTENT, intent);
                Exception resultException = null;

                try {
                    videoEditor.export(filename, height, bitrate, audioCodec, videoCodec, new ExportProgressListener() {
                        @Override
                        public void onProgress(VideoEditor videoEditor, String filename,
                                int progress) {
                            final Intent progressIntent = mIntentPool.get();
                            progressIntent.putExtra(PARAM_OP, OP_VIDEO_EDITOR_EXPORT_STATUS);
                            progressIntent.putExtra(PARAM_PROJECT_PATH, intent.getStringExtra(
                                    PARAM_PROJECT_PATH));
                            progressIntent.putExtra(PARAM_FILENAME, filename);
                            progressIntent.putExtra(PARAM_INTENT, intent);
                            progressIntent.putExtra(PARAM_PROGRESS_VALUE, progress);
                            mVideoThread.submit(progressIntent);
                            Log.v(TAG, "Export progress: " + progress + " for: " + filename);
                        }
                    });

                    statusIntent.putExtra(PARAM_CANCELLED, mExportCancelled);
                    if (!mExportCancelled) {
                        if (new File(filename).exists()) {
                        } else {
                            resultException = new IllegalStateException("Export file does not exist: " + filename);
                        }
                        Log.v(TAG, "Export complete for: " + filename);
                    } else {
                        Log.v(TAG, "Export cancelled by user, file name: " + filename);
                    }
                } catch (Exception ex) {
                    Log.v(TAG, "Export error for: " + filename);
                    ex.printStackTrace();
                    resultException = ex;
                }

                // Complete the request
                statusIntent.putExtra(PARAM_EXCEPTION, resultException);
                mVideoThread.submit(statusIntent);
            }
        }.start();
    }

    /**
     * Worker thread that processes intents and maintains its own intent queue.
     */
    private class IntentProcessor extends Thread {
        private final BlockingQueue<Intent> mIntentQueue;

        public IntentProcessor(String threadName) {
            super("IntentProcessor-" + threadName);
            mIntentQueue = new LinkedBlockingQueue<Intent>();
        }

        @Override
        public void run() {
            try {
                while(true) {
                    processIntent(mIntentQueue.take());
                }
            } catch (InterruptedException e) {
                Log.e(TAG, "Terminating " + getName());
            }
        }

        /**
         * Submits a new intent for processing.
         *
         * @param intent The intent to be processed
         */
        public void submit(Intent intent) {
            if (isAlive()) {
                mIntentQueue.add(intent);
            } else {
                Log.e(TAG, getName() + " should be started before submitting tasks.");
            }
        }

        /**
         * Removes an intent from the queue.
         *
         * @param intent The intent to be removed
         *
         * @return true if the intent is removed
         */
        public boolean cancel(Intent intent) {
            return mIntentQueue.remove(intent);
        }

        public Iterator<Intent> getIntentQueueIterator() {
            return mIntentQueue.iterator();
        }

        public void quit() {
            // Display an error if the queue is not empty and clear it.
            final int queueSize = mIntentQueue.size();
            if (queueSize > 0) {
                Log.e(TAG, "Thread queue is not empty. Size: " + queueSize);
                mIntentQueue.clear();
            }
            interrupt();
        }
    }

    /**
     * Returns a random String of numbers and letters (lower and upper case) of
     * the specified length. The method uses the Random class that is built-in
     * to Java which is suitable for low to medium grade security uses. This
     * means that the output is only pseudo random, i.e., each number is
     * mathematically generated so is not truly random.
     * <p>
     * The specified length must be at least one. If not, the method will return null.
     *
     * @param length the desired length of the random String to return.
     * @return a random String of numbers and letters of the specified length.
     */
    private static String randomString(int length) {
		final Random randGen = new Random();
		final char[] numbersAndLetters =
        	("0123456789abcdefghijklmnopqrstuvwxyz0123456789").toCharArray();
        if (length < 1) {
            return null;
        }
        // Create a char buffer to put random letters and numbers in.
        final char[] randBuffer = new char[length];
        for (int i = 0; i < randBuffer.length; i++) {
            randBuffer[i] = numbersAndLetters[randGen.nextInt(numbersAndLetters.length - 1)];
        }

        return new String(randBuffer);
    }

    /**
     * Delete all the files in the specified folder and the folder itself.
     *
     * @param dir The project path
     */
    private static boolean deleteDir(File dir) {
        if (dir.isDirectory()) {
            final String[] children = dir.list();
            for (int i = 0; i < children.length; i++) {
                final File f = new File(dir, children[i]);
                if (!deleteDir(f)) {
                    Log.e(TAG, "File cannot be deleted: " + f.getAbsolutePath());
                    return false;
                }
            }
        }

        // The directory is now empty so delete it
        return dir.delete();
    }
}
